<!--
  偏最小二乘 PLS
  1. 上传数据
  2.1 建模
  2.2 预测
  --------
  根据上传数据有无“Y”表来确定是否能建模
    有Y：可建模，也可读取模型
    无Y：不可建模，只能读取模型并做预测
 -->

<!--
  视图层
-->
<template><header-comp />

<!-- 功能简介 -->
<card-comp title="功能简介">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "To新手：
    　　可以读取示例数据，体验一下生成PLS模型、用模型预测等功能。
    Tips：
    　　训练好的模型可用于同自变量特征阵列、同因变量标签阵列的预测。
    　　即：自变量/因变量的样本数量可以改变，但自变量特征/因变量标签的种类或数量不能改变。"
  />
  <!-- 二级标题：PLS简介 -->
  <section-comp title="PLS简介">
    <!-- 文字内容 -->
    <view>
    　　偏最小二乘（Partial Least Squares, PLS），是经典的数据降维方法，用于解决“多重共线性”问题。<br />
    　　众多降维方法中，主成分分析（Principal Component Analysis, PCA）多用于非监督学习业务，其不区分自变量与因变量；线性判别分析（linear discriminant analysis, LDA）作为监督学习方法，其以因变量标签对自变量特征进行区分，用于分类业务；而PLS则既实现了有监督学习，又可用于回归业务，故在拉曼光谱数据处理中具有广泛应用。<br />
    　　具体算法介绍详见最下“算法介绍”部分。
    </view>
  </section-comp>
  <!-- 二级标题：参考文献 -->
  <section-comp title="参考文献">
    <!-- 文献 -->
    <view>
    　　文献链接：
    </view>
    <uni-link
      text="doi.org/10.5281/zenodo.7314532"
      href="https://doi.org/10.5281/zenodo.7314532"
      class="center" color="blue"
    />
    <!-- 算法 -->
    <view>
    　　算法链接：
    </view>
    <uni-link
      text="MATLAB算法（内附引用格式）"
      href="https://www.mathworks.com/matlabcentral/fileexchange/18760-partial-least-squares-and-discriminant-analysis"
      class="center" color="blue"
    />
    <!-- 库 -->
    <view>
    　　库链接：
    </view>
    <uni-link
      text="https://www.npmjs.com/package/ml-pls"
      href="https://www.npmjs.com/package/ml-pls"
      class="center" color="blue"
    />
  </section-comp>
</card-comp>

<!-- 读取数据文件组件 -->
<read-xlsx-data-comp
  noticeText=
    "需符合PLS格式要求的Excel表格，建议下载示例文件，直接在示例文件上修改后上传。
    或直接点击“读取示例数据”快速预览功能。
    注：如果只有“X”表，则只能进行【预测】（需上传PLS模型文件）。如果“X”表、“Y”表都有，则可【建模】，也可【预测】。具体详见示例文件。"
  visTab="PLS"
  exampleXlsxName="PLS数据集示例"
/>

<!-- PLS处理 -->
<card-comp title="PLS处理">

<!-- 条件渲染：没读取到有效PLS数据 -->
<view v-if="!isGetPlsDataRef">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "请先读取PLS数据"
  />
</view>

<!-- 条件渲染：读取到了有效PLS数据 -->
<view v-else>
  <!-- 建模 | 预测 功能切换 -->
  <uni-segmented-control
    :current="modelOrPredictIndexRef"
    :values="modelOrPredictRef"
    @clickItem="(event) => {
      modelOrPredictIndexRef = event.currentIndex
    }"
  />
  <!-- 建模 -->
  <view v-show="modelOrPredictRef[modelOrPredictIndexRef] === '建模'">
    <!-- 通告栏 -->
    <uni-notice-bar text=
      "构建并训练PLS模型。
      可设置参数条件，构建并下载PLS模型。
      也可针对“最大主成分数”参数摸条件，预览并下载最大主成分数与拟合质量趋势数据。"
    />
    <!-- 二级标题：参数介绍 -->
    <section-comp title="参数介绍">
      <!-- 文字内容 -->
      <view>
      ●　标准化：<br />
      　　标准化（Z-score Standardization），即依列(依X的特征、Y的标签)扣除X矩阵、Y矩阵格列均值，并除以标准差。让各列数据形成均值为0，标准差为1的数据。<br />
      　　该操作的目的是为了避免数据基础值不一致造成的各类问题。比如某些荧光鼓包，其信号强度很高，但是光强几乎不随待测物浓度变化而变化。此时在PLS提取方差和的最大列作为初始的“最能代表自变量的主成分”时，该荧光鼓包因方差和很大，就会被作为初始的X样本得分向量t——这是不符合光谱数据处理逻辑的。<br />
      　　因此，经过较多拉曼光谱数据实践，目前最新版本已强制开启“标准化”，不可关闭。<br />
      　--------<br />
      ●　相对容差：<br />
      　　容差值 = 相对容差 * 样本数。容差值在2处被用到：<br />
      　　1. 在“小迭代”中，当t向量在前后2次迭代的变化量的模小于等于预设的容差值时，则认为小迭代达到收敛条件，即“找到了最代表X、Y的向量”；<br />
      　　2. 在“大迭代”中，当X的残差矩阵E的模小于等于预设的容差值时，停止大迭代。<br />
      　　随着样本数量增加，X样本得分向量t、X残差矩阵E的模均会随之增加，即容差值的合适取值与数据样本量有关。因此，选取“相对容差”作为参数。<br />
      　　事实上，随着迭代进行，“小迭代”的t向量变化量的模总能小于容差值。但“大迭代”X残差矩阵E的模则与特征数、数据自身的线性质量等息息相关，常常无法收敛于容差值。则迭代收敛往往依靠“最大主成分数”达到要求。<br />
      　　默认“1e-5”，即“0.00001”。<br />
      　--------<br />
      ●　最大主成分数：<br />
      　　最大主成分数亦称“(最大)潜在向量数（Latent Vectors）”。当“大迭代”始终无法收敛时，“最大主成分数”即称为PLS模型迭代的停止条件：即每一次“大迭代”选出一个当前残差下的“第一主成分”，当选出的主成分数达到最大主成分数时，拟合停止。<br />
      　　最大主成分数最小为1，最大不应超过(样本数-1)或自变量特征数，即其最大值受样本维度和特征维度的制约。(样本数-1)：样本进行标准化操作后，引入了1个线性约束，即牺牲了1个统计自由度，因此为(样本数-1)；自变量特征数：这是数据降维的本质约束。<br />
      　　最大主成分数过小，则不足以提取出X、Y矩阵的“足够的有效信息”，导致拟合质量较差；最大主成分数过大，则除浪费算力以外，迭代中后期数据的线性已提取完毕，剩余的噪音数据使得X残差仍大于容差，但已无法继续提取出有效的t向量，这将导致主成分向量耗尽、主成分矩阵退化等问题，进一步引发程序报错。<br />
      　　因此，本程序设定的最大主成分默认值为：当(样本数-1)或自变量特征数小于10时，取两者最小值；当(样本数-1)或自变量特征数大于等于10时，取10。
      </view>
    </section-comp>
    <!-- 二级标题：参数设置 -->
    <section-comp title="参数设置">
      <!-- 文字内容 -->
      <view>
      　　可以填表输入参数，然后构建模型。<br />
      　　构建后，可下载模型。也可直接进入“预测”界面预测数据。<br />
      </view>
      <!-- 参数输入框的表格容器 -->
      <uni-table
        :border="true" :stripe="true"
      >
        <!-- 标题行 -->
        <uni-tr>
          <uni-th align="center">键</uni-th>
          <uni-th align="center">值</uni-th>
        </uni-tr>
        <!-- 内容行：标准化 -->
        <uni-tr>
          <uni-td align="center">标准化</uni-td>
          <uni-td align="center">
            关闭　<switch
              :checked="scaleRef"
              @change="(event) => { scaleRef = event.detail.value }"
              type="switch"
              :disabled="true"
            />　开启
          </uni-td>
        </uni-tr>
        <!-- 内容行：容差 -->
        <uni-tr>
          <uni-td align="center">相对容差</uni-td>
          <uni-td>
            <uni-easyinput
              v-model="relativeToleranceRef"
              type="number"
              placeholder="请输入相对容差值"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
        <!-- 内容行：最大主成分数 -->
        <uni-tr>
          <uni-td align="center">最大主成分数</uni-td>
          <uni-td>
            <uni-number-box
              class="center"
              v-model="latentVectorsRef"
              :min="1" :max="maxLatentVectorsRef" :step="1"
              :disabled="false"
            />
          </uni-td>
        </uni-tr>
      </uni-table>
    </section-comp>
    <!-- 建模按钮容器 -->
    <view class="center">
      <!-- 读取示例数据 -->
      <button
        @click="trainModel()"
        type="primary" size="mini" :plain="false"
        :disabled="false"
      >
        建立并训练PLS模型
      </button>
    </view>
    <!-- 下载模型按钮容器 -->
    <view
      v-show="isGetPlsModelRef"
      class="center"
    >
      <!-- 数据渲染图 -->
      <button
        @click="showVisor()"
        type="primary" size="mini" :plain="true"
        :disabled="false"
      >
        查看数据预览
      </button>
      <!-- 读取示例数据 -->
      <button
        @click="downloadModel()"
        type="primary" size="mini" :plain="true"
        :disabled="false"
      >
        下载PLS模型
      </button>
    </view>
    <!-- 二级标题：摸条件 -->
    <section-comp title="摸条件">
      <!-- 文字内容 -->
      <view>
      　　最大主成分数的选取对PLS模型的拟合质量有显著影响（详见最下“算法介绍”部分的“算法的不足”）。因此本程序提供“摸条件”功能，以帮助用户选取合适的最大主成分数。<br />
      　　输入“最大主成分数”参数的“起”、“止”值并点击“摸条件”按钮后，程序会读取“参数设置”部分的“标准化”、“相对容差”参数，然后从最大主成分的“起”值开始遍历，得到一系列迭代优化后的PLS模型。然后以各模型计算决定系数R²，记录不同最大主成分数下得到的模型R²，并绘制趋势图，以帮助用户选择合适的最大主成分数。<br />
      　　随着“最大主成分数”数增加：训练集中的有效信息会先被提取，R²呈现上升趋势；然后有效性较差的信息会被提取，R²开始呈现下降趋势；最后提取“看似相关，实则噪音”的信息，此时会出现主成分向量耗尽、主成分矩阵退化等问题。在数据样本量很小的情况下，甚至不会出现R²上升的过程。<br />
      　　用户可手动停止摸条件。摸条件结束或停止后，用户可预览趋势图，或下载趋势数据。
      </view>
      <!-- 参数输入框的表格容器 -->
      <uni-table
        :border="true" :stripe="true"
      >
        <!-- 标题行 -->
        <uni-tr>
          <uni-th align="center">主成分数</uni-th>
          <uni-th align="center">值</uni-th>
        </uni-tr>
        <!-- 内容行：主成分数：起 -->
        <uni-tr>
          <uni-td align="center">起</uni-td>
          <uni-td>
            <uni-number-box
              class="center"
              v-model="startLatentVectorsRef"
              :min="1" :max="endLatentVectorsRef" :step="1"
              :disabled="false"
            />
          </uni-td>
        </uni-tr>
        <!-- 内容行：主成分数：止 -->
        <uni-tr>
          <uni-td align="center">止</uni-td>
          <uni-td>
            <uni-number-box
              class="center"
              v-model="endLatentVectorsRef"
              :min="startLatentVectorsRef" :max="maxLatentVectorsRef" :step="1"
              :disabled="false"
            />
          </uni-td>
        </uni-tr>
      </uni-table>
      <!-- 摸条件按钮容器 -->
      <view class="center">
        <!-- 读取示例数据 -->
        <button
          @click="preTrainModel()"
          :type="!isPreTrainingRef ? 'primary' : 'warn'"
          size="mini" :plain="false"
          :disabled="false"
        >
          {{ !isPreTrainingRef ? "摸条件" : "停止摸条件" }}
        </button>
      </view>
      <!-- 摸条件数据呈现及下载容器 -->
      <view v-show="preTrainLogRef[1]">
        <!-- 摸条件记录模块 -->
        <uni-table
          :border="true" :stripe="true"
        >
          <!-- 标题行 -->
          <uni-tr>
            <uni-th align="center">主成分数</uni-th>
            <uni-th align="center">R²</uni-th>
            <uni-th align="center">是否收敛</uni-th>
          </uni-tr>
          <!-- 内容行 -->
          <uni-tr v-for="(log, index) of preTrainLogRef.slice(-10)" :key="index">
            <uni-td align="center">
              {{ log[0] }}
            </uni-td>
            <uni-td align="center">
              {{ log[1] }}
            </uni-td>
            <uni-td align="center">
              {{ log[2] }}
            </uni-td>
          </uni-tr>
        </uni-table>
        <!-- 数据渲染 | 下载记录 -->
        <view class="center">
          <!-- 数据渲染图 -->
          <button
            @click="showVisor()"
            type="primary" size="mini" :plain="true"
            :disabled="isPreTrainingRef"
          >
            {{ !isPreTrainingRef ? "查看数据预览图" : "迭代时无法查看预览" }}
          </button>
          <!-- 下载记录 -->
          <button
            @click="downloadPreTrainLog()"
            type="primary" size="mini" :plain="false"
            :disabled="isPreTrainingRef"
          >
            {{ !isPreTrainingRef ? "下载记录" : "迭代时无法下载记录" }}
          </button>
        </view>
      </view>
    </section-comp>
  </view>
  <!-- 预测 -->
  <view v-show="modelOrPredictRef[modelOrPredictIndexRef] === '预测'">
    <!-- 通告栏 -->
    <uni-notice-bar text=
      "读取PLS模型文件，或在“建模”模块构建PLS模型后，可一键生成并下载预测数据。"
    />
    <!-- 二级标题：读取模型文件 -->
    <section-comp title="读取模型">
      <!-- 文字内容 -->
      <view>
      　　在本“预测”模块，可点击读取此前在本程序“建模”模块训练并下载保存的PLS模型文件，以加载PLS模型。<br />
      　　当且仅当数据文件内包含“Y”标签的工作表，且在本程序“建模”模块训练了PLS模型时，本模块可自动读取该PLS模型文件。
      </view>
      <!-- 文件上传容器 -->
      <uni-file-picker
        :title="isGetPlsModelRef ? '已获得模型' : '点击读取模型文件：'"
        file-extname="xlsx, xls" fileMediatype="all"
        limit="1" return-type="object" :del-icon="false"
        :auto-upload="false" :disable-preview="true"
        :readonly="isGetPlsModelRef"
        @select="(event) => { readModelFile(event.tempFiles[0].file) }"
      >
        <!-- 插槽：按钮 -->
        <view class="center">
          <button
            type="primary" size="mini" :plain="false"
            :disabled="false"
          >
            选择模型文件
          </button>
        </view>
      </uni-file-picker>
    </section-comp>
    <!-- 二级标题：预测 -->
    <section-comp title="模型预测">
      <!-- 文字内容 -->
      <view>
      　　务必要注意的是，此前用于训练PLS模型的训练集自变量X数据，和待预测的自变量X数据，必须有相同数量、相同顺序的特征。<br />
      　　特征：如401、402、403、...、2000cm⁻¹波数的拉曼位移值，即为1600个自变量特征。<br />
      　　样本量不受影响，训练集数据用了300个样本的数据集，预测数据集数量不受限制，不必为300。<br />
      　　但特征必须一致！否则预测将无意义甚至会报错。
      </view>
      <!-- 模型预测按钮及容器 -->
      <view class="center">
        <button
          @click="modelPredict()"
          type="primary" size="mini" :plain="false"
          :disabled="!isGetPlsModelRef"
        >
          {{ isGetPlsModelRef ? "预测并下载结果" : "请先构建并训练PLS模型" }}
        </button>
      </view>
    </section-comp>
  </view>
</view>    

</card-comp>

<!-- 算法介绍 -->
<card-comp title="算法介绍">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "PLS算法的基本原理。"
  />
  <!-- 二级标题：简述 -->
  <section-comp title="简述">
    <!-- 文字内容 -->
    <view>
    　　PLS的降维算法发展自非线性迭代偏最小二乘（Non-linear Iterative Partial Least Squares, NIPALS），后又出现了简单偏最小二乘（Simple Partial Least Squares，SIMPLS）、核PLS（Kernel PLS）等众多PCA算法。本程序使用发展自NIPALS的经典PCA算法。即：NIPALS算法最早用于处理主成分分析（PCA）业务，只分解X矩阵，而PLS则在NIPALS思路的基础上同时分解X矩阵和Y矩阵。<br />
    　　PCA的核心原理是构建一个新的坐标轴，使得各数据点在该坐标轴上的投影，其平方和尽可能最大，该投影即为“第一主成分”。然后依次构建提取出第二主成分、第三主成分、...，通过更少的“主成分”就可以尽可能描述原坐标轴下各数据点的差异，以此实现数据的降维。而提取出的主成分则组成了“潜在空间”。LDA的核心原理则是力求在新的“潜在空间”中，同标签数据的方差和尽可能小，不同标签数据的方差尽可能大，以此实现了有监督的分类模型。PLS继承了PCA、LDA的降维思想并有所发展。其通过迭代提取潜在向量，每次提取的潜在向量均会力求其与自变量、因变量的协方差最大。即同时建立了自变量潜在空间的与因变量潜在空间，以更为综合的解决多重共线性问题。
    </view>
  </section-comp>
  <!-- 二级标题：算法参数 -->
  <section-comp title="算法参数">
    <!-- 文字内容 -->
    <view>
    ●　PLS算法涉及的基本参数如下：<br />
    　　n：样本数<br />
    　　p：自变量X的特征数<br />
    　　q：因变量Y的标签数<br />
    　　k：潜在向量数，潜在空间的维度，潜在特征数<br />
    　　X：自变量矩阵：n * p<br />
    　　Y：因变量矩阵：n * q<br />
    　--------<br />
    ●　PLS算法涉及的核心矩阵如下：<br />
    　　T：X的样本得分矩阵：n * k。即所构建的X的“潜在空间”，X的“主成分矩阵”<br />
    　　W：X的特征权重矩阵：p * k。由X和W可求得T：X * W = T<br />
    　　P：X的特征载荷矩阵：p * k。可用于由T计算X'<br />
    　　E：X的残差矩阵：n * p。X矩阵扣除主成分后剩余的矩阵残差：X = T * Pᵀ + E<br />
    　　U：Y的样本得分矩阵：n * k。即所构建的Y的“潜在空间”，Y的“主成分矩阵”<br />
    　　Q：Y的标签载荷矩阵：q * k。由Y和Q可求得U：U = Y * Q<br />
    　　F：Y的残差矩阵：n * q。Y矩阵扣除主成分后剩余的矩阵残差：Y = U * Qᵀ + F<br />
    　　B：回归系数矩阵：k * k。描述了X、Y的“潜在空间”之间的关系：U = T * B<br />
    　　PBQ：即“特征权重”，用于构建PLS回归模型的矩阵结合。PLS的回归模型为：<br />
    　　　　Y' = X' * P * B * Qᵀ<br />
    　　　令：P * B * Qᵀ = PBQ<br />
    　　　则：Y' = X' * PBQ<br />
    　　　即：PBQ可作为PLS算法的化归矩阵，在降维的基础上直接反映X与Y的回归关系<br />
    　--------<br />
    ●　PLS算法在迭代计算过程涉及到的过程向量如下：<br />
    　　t：X的样本得分向量：n * 1。每次向量迭代时，首先选当次残差X的方差和最大的列为t，即“最能代表X的主成分”。详见下“t”。<br />
    　　u：Y的样本得分向量：n * 1。每次向量迭代时，首先选当次残差Y的方差和最大的列为u，即“最能代表Y的主成分”。详见下“u”。<br />
    　　w：X的特征权重向量：p * 1。计算：w = norm(Xᵀ * u)。即X每个特征的所有样本与[最能代表Y的u向量]的二乘和，再归一化。即计算对X而言的各X特征的X-Y二乘指数，所得w即为X各特征综合考虑了X、Y的变化程度(变异)以及X、Y的相关性的权重。并以norm()取模以归一化w，使w只具备“方向性”。w向量中，元素的值越大，则该特征就越具备[X、Y的变化范围越大]与[X、Y相关性越强]这两个综合特征（此思路即为“协方差最大化”，或“最大协方差”）。<br />
    　　t：X的样本得分向量：n * 1。计算：t = X * w。即X每个样本的所有特征与[X的特征权重向量w]的二乘和。t向量体现的是以当前各特征权重w对样本加权后，每个样本对整体X的影响/代表能力。从主成分提取角度而言，t即从当前X中找到的，“潜在空间”的最大投影向量(第一主成分)，亦即“最能代表X的主成分”。<br />
    　　q：Y的标签载荷向量：q * 1。计算：q = norm(Yᵀ * t)。即Y每个标签的所有样本与[最能代表X的t向量]的二乘和，再归一化。与w类似，q向量中，元素的值越大，则该标签就越具备[X、Y的变化范围越大]与[X、Y相关性越强]这两个综合特征。<br />
    　　u：Y的样本得分向量：n * 1。计算：u = Y * q。与t类似，u即为“最能代表Y的主成分”。<br />
    　　p：X的特征载荷向量：p * 1。在得到“最能代表X的主成分”t向量后，需要得到[扣除该主成分后剩下的X残差]，才能以残差进入下一轮的迭代，继续提取残差中的主成分。因此需要实现t转换回原空间，即t => X'，然后通过E = X - X'得到残差。另一方面，拟合公式上的Y项和X项其实都是扣除各自残差后的Y'和X'项，因此有定义式：X' * p = t，同时t能通过pᵀ逆推回X'，则计算式：p = norm[(Xᵀ * t) / (tᵀ * t)]。<br />
    　　b：回归系数标量。定义式：u = t * b，即拟合回归系数。计算式：b = (tᵀ * u) / (tᵀ * t)。
    </view>
  </section-comp>
  <!-- 二级标题：计算过程 -->
  <section-comp title="计算过程">
    <!-- 文字内容 -->
    <view>
    　PLS算法计算过程简述如下：<br />
    　1. “大迭代”，即每次迭代都以下述的“小迭代”为算法，生成一个潜在空间向量（一个主成分）。<br />
    　　每一次“大迭代”获得主成分后，均会从X、Y矩中阵扣除此前历次得到的主成分矩阵，获得X、Y的残差矩阵。下一次“大迭代”即从X、Y残差矩阵中获得“剩余残差中的最大主成分”。<br />
    　　通过多次“大迭代”，当生成的潜在空间向量数（主成分数）满足要求（达到预设的“最大主成分”数）时，或X的残差矩阵E的模小于等于预设的容差值时，停止大迭代。<br />
    　　大迭代获得的由多个主成分的各类向量汇总的潜在空间矩阵，即为最终求得的各类矩阵。<br />
    　2. “小迭代”，即以“最能代表X、Y矩阵的向量”为目标的迭代过程：<br />
    　　第0步：选取X矩阵的特征(列)平方和最大的X特征列，作为初始的X样本得分向量t；选取标签(列)平方和最大的Y标签列，作为初始的Y样本得分向量u。<br />
    　　第1步：w = norm(Xᵀ * u)。以[最代表Y的u向量]与X求得X各特征的归一化权重w。<br />
    　　第2步：t = X * w。以X各特征的归一化权重w求得“最代表X”的t向量。<br />
    　　第3步：q = norm(Yᵀ * t)。类似第1步，以[最代表X的t向量]与Y求得Y各特征的归一化权重q。<br />
    　　第4步：u = Y * q。类似第2步，以Y各特征的归一化权重q求得“最代表Y”的u向量。<br />
    　　反复执行上述1~4步，每完整执行一次1~4步，称为一次“小迭代”。每次小迭代都会更新X、Y的得分向量t、u，即更新“最代表X、Y的向量”。<br />
    　　若t向量在前后2次迭代的变化量的模小于等于预设的容限值，则认为小迭代达到收敛条件，即“找到了最代表X、Y的向量”（当前迭代下的最优第一主成分）。则结束本轮小迭代，提取优化得到的t、u向量添加进T、U矩阵。<br />
    　　然后：p = norm[(Xᵀ * t) / (tᵀ * t)]；X' = t * pᵀ；E = X - X'；Y' = T * B；F = Y - Y'……得到X、Y的残差矩阵E、F。然后进行下一轮大迭代，下一轮的大迭代将以E、F为新的X、Y，开始“最代表新X、新Y的向量”（残差的最优第一主成分）的小迭代过程。
    </view>
  </section-comp>
  <!-- 二级标题：算法的不足 -->
  <section-comp title="算法的不足">
    <!-- 文字内容 -->
    <view>
    　1. R²问题<br />
    　　从PLS算法原理可知，其对自变量X矩阵、因变量Y矩阵的特征提取，意在提取其“最具相关性的主成分”，其迭代收敛的指标为自变量X矩阵在主成分提取后的残差矩阵E。<br />
    　　然而在实际应用场景中，模型的评价指标应为R²。特别地，为消除不同因变量标签的量纲影响，具体可落地的评价指标应该为标准化后各因变量标签的综合决定系数R²。<br />
    　　可惜的是，PLS算法在拟合迭代过程中，并不以R²为指标。事实上，其运算过程中无法获得R²，除非以既定参数拟合迭代完毕。<br />
    　　不仅如此，PLS算法的拟合回归并不是以X矩阵和Y矩阵进行的，而是以T矩阵和U矩阵进行的。U矩阵与X、Y矩阵之间，存在着残差矩阵E、F。这进一步降低了模型对训练集的拟合精度R²，亦即提高了模型拟合成本。<br />
    　　在此情况下，想用PLS算法得到理想的回归模型，往往需要以R²为指标，先找出最合适的主成分数量。这也就是本程序“摸条件”功能的由来。<br />
    　2. 线性问题<br />
    　　就实际业务而言，可用于回归的“主成分”，很可能是两个拉曼位移处的峰强之比——这是典型的非线性问题。然而目前PLS算法对主成分的提取，仅仅是原X矩阵数据的线性加和，这是无法解决非线性问题的。<br />
    　　因此，在PLS算法中，如果自变量X矩阵中存在非线性关系，则需要对X矩阵进行非线性变换，再进行PLS算法拟合。或者，应该探索具备非线性功能的算法，如核偏最小二乘（K-PLS）、核正交偏最小二乘（K-OPLS）等。
    </view>
  </section-comp>
</card-comp>

<!-- 页脚(下) -->
<footer-comp /></template>

<!--
  逻辑层
-->
<script setup>
// 引入VUE框架方法
import { onMounted, watch, shallowRef, ref } from "vue"
// 引入各类方法
import { sheetToAoa, aoaTranspose, aoaMapToWorkbook, downloadXlsx, readXlsxFile } from "@/assets/app-xlsx.js"
import { mlMatrixToAoa } from "@/assets/app-ml-utils.js"
import { showVisor, renderScatterPlot } from "@/assets/app-tfjs-vis.js"
// 引入全局状态管理对象
import { readFileStore } from "@/stores/read-file.js"
// 导入ml-matrix库
import { Matrix } from "ml-matrix"
// 导入ml-pls库
import { PLS } from "ml-pls"

/** 初始化全局状态管理对象：谱图数据读取状态 */
const readFile = readFileStore()
/** Ref状态：是否读取到有效的PLS数据文件 */
const isGetPlsDataRef = shallowRef(false)
/** Ref状态：建模or预测选项数组 */
const modelOrPredictRef = shallowRef(["预测"])
/** Ref状态：建模or预测选择index */
const modelOrPredictIndexRef = shallowRef(0)
/** Ref状态：是否获取到PLS模型 */
const isGetPlsModelRef = shallowRef(false)
/** Ref状态：PLS建模参数-标准化 */
const scaleRef = shallowRef(true)
/** Ref状态：PLS建模参数-主成分数 */
const latentVectorsRef = shallowRef(1)
/** Ref状态：PLS建模参数-最大主成分数 */
const maxLatentVectorsRef = shallowRef(1)
/** Ref状态：相对容差 */
const relativeToleranceRef = shallowRef(1e-5)
/** Ref状态：主成分数：起 */
const startLatentVectorsRef = shallowRef(1)
/** Ref状态：主成分数：止 */
const endLatentVectorsRef = shallowRef(1)
/** Ref状态：摸条件记录 */
const preTrainLogRef = ref([])
/** Ref状态：正在预训练 */
const isPreTrainingRef = shallowRef(false)

/**
 * pls项目的数据对象
 * @type { Object } plsObject
 * @type { (String | Number)[] } plsObject.samples - 样品名
 * @type { (String | Number)[] } plsObject.xFeatures - X特征名
 * @type { (String | Number)[] } plsObject.yLabels - Y标签名
 * @type { Matrix } plsObject.xMatrix - X数据，Matrix矩阵
 * @type { Matrix } plsObject.yMatrix - Y数据，Matrix矩阵
 * @type { PLS } plsObject.plsModel - PLS模型
 */
const plsObject = {
  samples: [],
  xFeatures: [],
  yLabels: [],
  xMatrix: null,
  yMatrix: null,
  plsModel: null
}


// 生命周期函数，页面栈挂载完成执行
onMounted(() => {
  // 注册监听：谱图数据读取状态
  watch(readFile, onReadFile)
})

/**
 * 谱图数据读取状态变化的监听回调
 * @param { Store<readFile> } newReadFile 新的初始化全局状态管理对象
 */
async function onReadFile(newReadFile) { try {
  // 如果没读到文件，那就直接返回即可
  if (newReadFile.isGetFile === false) { return }
  // 显示加载圈
  await uni.loading("正在读取数据")
  // 数据检查
  const dataVerified = dataVerify(newReadFile.workbook)
  // 如果数据检查失败，直接返回即可
  if (dataVerified === null) { return }
  // 获取最大主成分数
  const maxLatentVector = Math.min(
    // 样本数 - 1
    dataVerified.xMatrix.rows - 1,
    // 特征数
    dataVerified.xMatrix.columns
  )
  // 最大主成分数赋值
  maxLatentVectorsRef.value = maxLatentVector
  // 主成分数赋值
  latentVectorsRef.value = Math.min(maxLatentVector, 10)
  // 主成分数：止 赋值
  endLatentVectorsRef.value = maxLatentVector
  // 如果读到Y，则新增建模选项
  if (dataVerified.yLabels) {
    modelOrPredictRef.value.push("建模")
    // 同时选项卡默认到“建模”
    modelOrPredictIndexRef.value = 1
  }
  // 给PLS项目数据对象赋值
  plsObject.samples = dataVerified.samples
  plsObject.xFeatures = dataVerified.xFeatures
  plsObject.xMatrix = dataVerified.xMatrix
  plsObject.yLabels = dataVerified.yLabels || null
  plsObject.yMatrix = dataVerified.yMatrix || null
  // 更改Ref状态
  isGetPlsDataRef.value = true
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  // 隐藏加载圈
  uni.hideLoading()
  console.error("onReadFile()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "读取数据报错",
    content: error.message,
  })
}}

/**
 * 工作簿数据验证
 * @param { XLSX.WorkBook } workbook Excel工作簿对象
 * @returns {{
     samples: (String | Number)[],
     xFeatures: (String | Number)[],
     xMatrix: Matrix,
     yLabels?: (String | Number)[],
     yMatrix?: Matrix
   }} 处理后的数据
 * 验证内容：特征(自变量)X表(必须得有)，标签(因变量)Y表(可以没有)
 */
function dataVerify(workbook) { try {
  // 装一个错误框
  const checkBugs = []
  /**
   * @验证X表
   */
  // 获取工作簿的特征(自变量)X表，转为AOA格式
  const aoaX = sheetToAoa(workbook, "X")
  // 如果aoaX表第1行长度为1，且第一行第1个不是“X”，则为备注信息，则就地删除aoaX表第1行
  if ((aoaX[0].length === 1) && (aoaX[0][0] !== "X")) { aoaX.shift() }
  // 然后提取出标题数组
  const titleArr = aoaX[0]
  // 开始做PLS-X数据检查
  // 第一个标题必须为“X”
  if (String(titleArr[0]).trim().toUpperCase() !== "X") {
    checkBugs.push("自变量X表首列数据标签不是“X”，请检查")
  }
  /**
   * @验证Y表
   */
  // 获取工作簿的标签(因变量)Y表，转为AOA格式
  const aoaY = (workbook.SheetNames.indexOf("Y") === -1)
    ? null
    : sheetToAoa(workbook, "Y")
  // 如果获取到了Y的数据
  if (aoaY !== null) {
    // 如果aoaY表第1行长度为1，且第一行以“【”起始，则为备注信息，则就地删除aoaY表第1行
    if (
      (aoaY[0].length === 1)
        && (aoaY[0][0].slice(0, 1) === "【")
    ) {
      aoaY.shift()
    }
    // 验证Y样本数和X样本数(含标签)是否一致
    if (aoaY.length !== titleArr.length) {
      checkBugs.push("自变量X表和因变量Y表的样品数不一致，请检查")
    }
    // 若无错误，则比对X和Y的数据标签一致性
    if (checkBugs.length < 1) {
      labelEqual: for (let i = 1; i < titleArr.length; i++) {
        // 如果标签不一致，则报错，并跳出循环
        if (titleArr[i] !== aoaY[i][0]) {
          checkBugs.push(
            `自变量X表和因变量Y表从第 ${ i + 1 } 个样本开始出现标签不一致的情况，请检查`
          )
          break labelEqual
        // 否则，将标签添加到标签数组中
        }
      }
    }
  }
  // 如果有错误，则提示并返回
  if (checkBugs.length > 0) {
    uni.showModal({
      showCancel: false,
      title: "数据有误",
      content: checkBugs.toString(),
    })
    return null
  // 否则，即数据无误，
  } else {
    // 处理X、Y数组，以及标签和特征，并导出
    return dataProcess({ aoaX, aoaY })
  }
} catch (error) {
  console.error("dataVerify()报错: ", error)
  // 被调用方法，直接抛出错误即可
  throw new Error(error)
}}

/**
 * 数据处理，被数据验证dataVerify()直接回调
 * @param {{ aoaX: (string | number)[][], aoaY: (string | number)[][] }} data 数据
 * @returns {{
     samples: (String | Number)[],
     xFeatures: (String | Number)[],
     xMatrix: Matrix,
     yLabels?: (String | Number)[],
     yMatrix?: Matrix
   }} 处理后的数据
 * 处理内容：样本名称samples，特征xFeatures，X矩阵xMatrix，标签yLabels，Y矩阵yMatrix
 */
function dataProcess(data) { try {
  // 接传参
  const { aoaX: dataXAoaTemp, aoaY: dataYAoaTemp } = data
  // X数组第一行是样本名称samples + 前面带了个“X”的标签，需要去掉
  const samples = dataXAoaTemp.shift()
  // 去掉“X”
  samples.shift()
  // 转置数组X，则第一行就是特征xFeatures
  const dataXAoa = aoaTranspose(dataXAoaTemp)
  // aoaX就地删除第一行，得特征xFeatures
  const xFeatures = dataXAoa.shift()
  // dataXAoa转矩阵
  const xMatrix = new Matrix(dataXAoa)
  // 如果Y表为空，则直接返回
  if (dataYAoaTemp === null) { 
    return { samples, xFeatures, xMatrix }
  }
  // aoaY第一行就是yLabels
  const yLabels = dataYAoaTemp.shift()
  // 但是aoaY第一行多了个“数据名”列，要去掉
  yLabels.shift()
  // 处理获得Y矩阵aoaY
  const dataYAoa = []
  for (let i = 0; i < dataYAoaTemp.length; i++) {
    // 去掉samples列
    dataYAoaTemp[i].shift()
    // 然后添加到dataYAoa中
    dataYAoa.push(dataYAoaTemp[i])
  }
  // dataYAoa转矩阵
  const yMatrix = new Matrix(dataYAoa)
  // 返回数据
  return { samples, xFeatures, xMatrix, yLabels, yMatrix }
} catch (error) {
  console.error("dataProcess()报错: ", error)
  // 被调用方法，直接抛出错误即可
  throw new Error(error)
}}

/**
 * 训练模型
 * @note 建模阶段，会读取latentVectorsRef、relativeToleranceRef、plsObject.samples、scaleRef
 * @note 训练阶段，会读取plsObject.xMatrix、plsObject.yMatrix
 * @note 成功建模并训练后，会将模型赋值给plsObject.pls
 */
async function trainModel() { try {
  // 显示加载圈
  await uni.loading("正在训练模型")
  // 接收传参
  const options = {
    latentVectors: latentVectorsRef.value,
    tolerance: Number(relativeToleranceRef.value) * plsObject.samples.length,
    scale: scaleRef.value
  }
  // 建模
  const pls = new PLS(options)
  // 训练
  pls.train(plsObject.xMatrix.clone(), plsObject.yMatrix.clone())
  // PBQ绘图
  pbqPlot(pls.PBQ)
  // 模型赋值给plsObject
  plsObject.plsModel = null
  plsObject.plsModel = pls
  // 修改Ref值：获取到了PLS模型
  isGetPlsModelRef.value = true
  // 完成，隐藏加载圈
  uni.hideLoading()
} catch (error) {
  uni.hideLoading()
  console.error("trainModel()报错: ", error)
  // 根据报错信息分类处理
  if (error.message === "Submatrix indices are out of range") {
    uni.showModal({
      showCancel: false,
      title: "PLS模型训练报错",
      content: "主成分向量耗尽、主成分矩阵退化，请减小最大主成分，或增大容差",
    })
  } else {
    uni.showModal({
      showCancel: false,
      title: "PLS模型训练报错",
      content: error.message,
    })
  }
}}

/**
 * 下载模型
 * @note 会读取plsObject.plsModel模型文件
 */
function downloadModel() { try {
  // 接模型
  const plsModel = plsObject.plsModel
  // 如果模型不存在，则提示用户
  if (!plsModel) { 
    throw new Error("模型不存在，请先训练模型")
  }
  // 构建参数数组
  const mainParamArr = []
  // 添加参数
  mainParamArr.push(["name", "PLS"])
  mainParamArr.push(["scale", plsModel.scale])
  mainParamArr.push(["tolerance", plsModel.tolerance])
  mainParamArr.push(["latentVectors", plsModel.latentVectors])
  mainParamArr.push(["R2X", plsModel.R2X])
  mainParamArr.push(["ssqYcal", plsModel.ssqYcal])
  // 构建AOA键值对
  const aoaMap = new Map()
  // 添加键值对：基本参数
  aoaMap.set("main", mainParamArr)
  // 添加键值对：数组（需要扩围后转置）
  aoaMap.set("meanX", aoaTranspose([plsModel.meanX]))
  aoaMap.set("stdDevX", aoaTranspose([plsModel.stdDevX]))
  aoaMap.set("meanY", aoaTranspose([plsModel.meanY]))
  aoaMap.set("stdDevY", aoaTranspose([plsModel.stdDevY]))
  // 添加键值对：矩阵
  aoaMap.set("PBQ", mlMatrixToAoa(plsModel.PBQ))
  aoaMap.set("W", mlMatrixToAoa(plsModel.W))
  aoaMap.set("T", mlMatrixToAoa(plsModel.T))
  aoaMap.set("P", mlMatrixToAoa(plsModel.P))
  aoaMap.set("E", mlMatrixToAoa(plsModel.E))
  aoaMap.set("Q", mlMatrixToAoa(plsModel.Q))
  aoaMap.set("U", mlMatrixToAoa(plsModel.U))
  aoaMap.set("F", mlMatrixToAoa(plsModel.F))
  aoaMap.set("B", mlMatrixToAoa(plsModel.B))
  // 构建workbook工作簿对象
  const workbook = aoaMapToWorkbook(aoaMap)
  // 下载对象
  downloadXlsx(workbook, "plsModel.xlsx")
} catch (error) {
  console.error("downloadModel()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "模型下载报错",
    content: error.message,
  })
}}

/**
 * 读取模型
 * @param { File } file Excel文件对象
 * @note 会写入模型文件plsObject.plsModel
 */
async function readModelFile(file) { try {
  // 显示加载圈
  await uni.loading("正在读取模型")
  // 读取模型文件
  const workbook = await readXlsxFile(file)
  // pls参数对象
  const plsParams = {}
  // 获取基本参数
  const mainParamAoa = sheetToAoa(workbook, "main")
  // 拆解基本参数
  for (let i = 0; i < mainParamAoa.length; i++) {
    // 写入参数对象
    plsParams[mainParamAoa[i][0]] = mainParamAoa[i][1]
  }
  // 获取数组
  plsParams["meanX"] = aoaTranspose(sheetToAoa(workbook, "meanX"))[0]
  plsParams["stdDevX"] = aoaTranspose(sheetToAoa(workbook, "stdDevX"))[0]
  plsParams["meanY"] = aoaTranspose(sheetToAoa(workbook, "meanY"))[0]
  plsParams["stdDevY"] = aoaTranspose(sheetToAoa(workbook, "stdDevY"))[0]
  // 获取矩阵
  plsParams["PBQ"] = new Matrix(sheetToAoa(workbook, "PBQ"))
  // 其它矩阵可有可无
  // plsParams["W"] = new Matrix(sheetToAoa(workbook, "W"))
  // plsParams["T"] = new Matrix(sheetToAoa(workbook, "T"))
  // plsParams["P"] = new Matrix(sheetToAoa(workbook, "P"))
  // plsParams["E"] = new Matrix(sheetToAoa(workbook, "E"))
  // plsParams["Q"] = new Matrix(sheetToAoa(workbook, "Q"))
  // plsParams["U"] = new Matrix(sheetToAoa(workbook, "U"))
  // plsParams["F"] = new Matrix(sheetToAoa(workbook, "F"))
  // plsParams["B"] = new Matrix(sheetToAoa(workbook, "B"))
  const plsModel = PLS.load(plsParams)
  // PBQ绘图
  pbqPlot(plsModel.PBQ)
  // 绘图成功，赋值
  plsObject.plsModel = null
  plsObject.plsModel = plsModel
  // 修改Ref值：获取到了PLS模型
  isGetPlsModelRef.value = true
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  // 隐藏加载圈
  uni.hideLoading()
  console.error("readModel()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "读取模型报错",
    content: error.message,
  })
}}

/**
 * PBQ作图
 * @param { Matrix } pbq PLS模型的PBQ矩阵
 * @note 会读取plsObject.xFeatures、plsObject.yLabels
 */
function pbqPlot(pbq) { try {
  // 数据检查，确定PBQ不包含NaN
  const columnNumber = pbq.columns
  const rowNumber = pbq.rows
  for (let row = 0; row < rowNumber; row++) {
    for (let col = 0; col < columnNumber; col++) {
      if (Number.isNaN(pbq.get(row, col))) {
        throw new Error("模型拟合失败，请检查相关性")
      }
    }
  }
  // 检查完毕。用PBQ的每一列列数据作图
  const xFeatures = plsObject.xFeatures
  const yLabels = plsObject.yLabels
  for (let col = 0; col < columnNumber; col++) {
    renderScatterPlot(
      { tab: "PLS", name: yLabels[col] },
      [{ x: xFeatures, y: pbq.getColumn(col) }],
      [yLabels[col]],
      { yLabel: "PQB (P * Q * Bᵀ)" },
    )
  }
  // 打开图表栏
  showVisor()
} catch (error) {
  console.error("pbqPlot()报错: ", error)
  // 被调用方法，直接抛出错误即可
  throw new Error(error)
}}

/**
 * 摸条件预训练模型的signal判断
 * @note 预训练阶段，会读取plsObject.xMatrix，plsObject.yMatrix
 * @note startLatentVectorsRef，endLatentVectorsRef，relativeToleranceRef，plsObject.samples，scaleRef
 */
async function preTrainModel() { try {  
  // 如果没有在预训练
  if (!isPreTrainingRef.value) {
    // 清空预训练记录
    preTrainLogRef.value.length = 0
    // 调整成预训练
    isPreTrainingRef.value = true
    // 接传参
    const startLatentVector = startLatentVectorsRef.value
    const endLatentVector = endLatentVectorsRef.value
    const tolerance = Number(relativeToleranceRef.value) * plsObject.samples.length
    const scale = scaleRef.value
    const xMatrix = plsObject.xMatrix
    const yMatrix = plsObject.yMatrix
    // 迭代
    // for...await方法，异步迭代器，配合Promise + setTimeout回调封装，实现任务池非阻塞
    for await (const result of preTrainGenerator(
      startLatentVector, endLatentVector, tolerance, scale, xMatrix, yMatrix
    )) {
      // 记录训练结果
      preTrainLogRef.value.push(result)
    }
    // 循环结束，关闭预训练
    isPreTrainingRef.value = false
    // 绘图，先获取数据
    const preTrainLog = aoaTranspose(preTrainLogRef.value, 2)
    // 绘图预览
    renderScatterPlot(
      { tab: "PLS", name: "摸条件" },
      [{ x: preTrainLog[0], y: preTrainLog[1] }],
      ["趋势"],
      { xLabel: "最大主成分数", yLabel: "R²" },
    )
    // 打开图表栏
    showVisor()
  // 反之如果已经在训练了
  } else {
    // 调整成非预训练
    isPreTrainingRef.value = false
  }
} catch (error) {
  console.error("preTrainModel()报错: ", error)
  // 调整成非预训练
  isPreTrainingRef.value = false
  uni.showModal({
    showCancel: false,
    title: "摸条件报错",
    content: error.message,
  })
}}

/**
 * 摸条件的迭代器
 * @param { Number } startLatentVector 最大主成分：起
 * @param { Number } endLatentVector 最大主成分：止
 * @param { Number } tolerance 容差
 * @param { Boolean } scale 是否归一化
 * @param { Matrix } xMatrix X矩阵数据
 * @param { Matrix } yMatrix Y矩阵数据
 * @returns { Number[] } 分别是遍历到的最大主成分和R2X
 */
async function* preTrainGenerator(
  startLatentVector, endLatentVector, tolerance, scale, xMatrix, yMatrix
) { try {
  // 遍历迭代
  for (let i = startLatentVector; i <= endLatentVector; i++) {
    // 如果关闭了，那就停止
    if (isPreTrainingRef.value === false) { return }
    // 接收传参
    const options = {
      latentVectors: i,
      tolerance: tolerance,
      scale: scale
    }
    // 建模
    const pls = new PLS(options)
    // 训练
    pls.train(xMatrix.clone(), yMatrix.clone())
    if (Number.isNaN(pls.R2X)) {
      throw new Error(`最大主成分数为 ${ i } 时，矩阵迭代出现极小值向量，请减小最大主成分，或增大容差`)
    }
    // 计算R²：
    // 预测Y，就地扣除Y真实值，得到残差，然后标准化
    // [Y(预测) - Y(观测)]各自标准化，把减去均值meanY这一步抵消了
    const yMatrixResidualScale = pls.predict(xMatrix.clone()).sub(yMatrix).divRowVector(pls.stdDevY)
    // 计算残差平方和，进一步计算R2
    // R2 = 1 - 残差平方和 / 原平方和
    const R2 = 1
      - (yMatrixResidualScale.clone().mul(yMatrixResidualScale).sum() / pls.ssqYcal)
    // 计算E和F的模，即X、Y的残差的模
    const normE = pls.E.norm()
    const normF = pls.F.norm()
    // 确定拟合收敛条件
    const isConvergence = (normE > tolerance) ? "N" : "Y"
    // 返回：构建强制异步，给视图层刷新留时间
    yield new Promise((resolve) => {
      // 用setTimeout()封装一个强行异步
      // 用箭头函数封装resolve()，不直接实现resolve结果，而是强行异步实现resolve()
      setTimeout(() => { resolve([i, R2, isConvergence, normE, normF]) })
    })
  }
} catch (error) {
  console.error("preTrainGenerator()报错: ", error)
  // 被调用方法，直接抛出错误即可
  throw new Error(error)
}}

/**
 * 下载记录表
 */
function downloadPreTrainLog() { try {
  // 提取数据并装箱
  const aoa = [
    ["最大主成分", "R²", "是否收敛", "X残差模", "Y残差模"],
    ...preTrainLogRef.value
  ]
  // 构建aoaMap
  const map = new Map()
  map.set("摸条件", aoa)
  // aoaMap数据转workbook工作簿
  const workbook = aoaMapToWorkbook(map)
  // 下载
  downloadXlsx(workbook, "PLS摸条件记录.xlsx")
} catch (error) {
  console.error("downloadPreTrainLog()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "下载记录报错",
    content: error.message,
  })
}}

/**
 * 预测(并下载结果)
 * @note 预测阶段，会读取plsObject.plsModel、plsObject.xMatrix
 * @note 下载阶段，会读取plsObject.samples、plsObject.yLabels
 */
async function modelPredict() { try {
  // 显示加载圈
  await uni.loading("正在预测")
  // 接模型和X
  const plsModel = plsObject.plsModel
  const xMatrix = plsObject.xMatrix
  // 预测
  const yMatrixPredict = plsModel.predict(xMatrix)
  // 装箱
  const colNumber = yMatrixPredict.columns
  const rowNumber = yMatrixPredict.rows
  const yPredictAoa = [["样品", ...plsObject.yLabels]]
  for (let row = 0; row < rowNumber; row++) {
    // 先加一个样品名
    yPredictAoa.push([plsObject.samples[row]])
    // 填充数据
    for (let col = 0; col < colNumber; col++) {
      yPredictAoa[row + 1].push(yMatrixPredict.get(row, col))
    }
  }
  // 处理完毕，构建Map数据
  const yMap = new Map()
  yMap.set("yPredict", yPredictAoa)
  // Map数据转workbook工作簿
  const workbook = aoaMapToWorkbook(yMap)
  // 下载
  downloadXlsx(workbook, "plsPredict.xlsx")
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  // 隐藏加载圈
  uni.hideLoading()
  console.error("modelPredict()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "模型预测报错",
    content: error.message,
  })
}}

/**
 * 测试方法
 * @param { Event } event
 */
function test(event) {
  console.log("event: ", event)
}

</script>
